﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using RoketPack.Management;
using System.Xml;
using RoketPack.Collections;
using System.IO;
using RoketPack.Management.Detection;
using RoketPack.Management.Objects;
using System.Drawing;

namespace RoketPack
{
    class ConfigurationReader
    {
        private XmlReader m_XmlReader = null;

        /// <summary>
        /// Fetchs a list of repositories from the local Repositories.xml document
        /// and returns them as a list of Repository instances.
        /// </summary>
        /// <returns>A list containing all of the repositories.</returns>
        internal List<Repository> GetRepositoryList()
        {
            Tree t = null;
            try
            {
                this.m_XmlReader = XmlReader.Create("Repositories.xml");
                t = Tree.FromXml(this.m_XmlReader);
            }
            finally
            {
                this.m_XmlReader.Close();
            }
            List<Repository> repos = new List<Repository>();

            // Read the tree to get the repository information.
            Node rn = t.GetChildElement("repositories");

            if (rn == null)
                throw new XmlException("The Repositories.xml document does not contain a root <repositories> node.");

            bool hasre = false;
            foreach (Node rx in rn.GetChildElements("repo"))
            {
                string url = rx.Attributes["url"];
                string name = null;
                string description = null;
                if (rx.GetChildElement("name").GetText() != null)
                    name = rx.GetChildElement("name").GetText().Value;
                if (rx.GetChildElement("description").GetText() != null)
                    description = rx.GetChildElement("description").GetText().Value;

                if (!hasre && url.StartsWith("https://pack.roket-enterprises.com"))
                    hasre = true;

                repos.Add(new Repository(url, name, description));
            }

            // Check to ensure that the official repository (containing the update url for
            // RoketPack itself) is included.
            if (!hasre)
            {
                repos.Insert(0, new Repository(
                    "https://pack.roket-enterprises.com",
                    "Official Software Repository",
                    "The official software repository for software written or endorsed by Roket Enterprises.")
                    );
            }

            return repos;
        }

        /// <summary>
        /// Interprets a list of applications from a software.xml document.
        /// </summary>
        /// <param name="data">The XML data.</param>
        /// <param name="repourl">The repository URL to add to the relative URLs stored in the XML data.</param>
        /// <returns>A dictionary containing all of the repositories (the IDs are the keys).</returns>
        internal Dictionary<string, Application> GetApplicationList(byte[] data, Repository repo)
        {
            this.m_XmlReader = XmlReader.Create(new MemoryStream(data));
            Tree t = Tree.FromXml(this.m_XmlReader);
            this.m_XmlReader.Close();
            Dictionary<string, Application> apps = new Dictionary<string, Application>();

            // Read the tree to get the list information.
            Node sn = t.GetChildElement("software");

            if (sn == null)
                throw new XmlException("The software.xml document does not contain a root <software> node.");

            foreach (Node app in sn.GetChildElements("application"))
            {
                string id = app.Attributes["id"];
                string url = app.GetText().Value;

                ConfigurationReader config = new ConfigurationReader();
                Application a = config.GetApplicationData(
                    Cache.GetApplicationData(repo.Url, id, repo.Url + "/" + url),
                    repo,
                    repo.Url + "/" + url);
                apps.Add(id, a);
            }

            return apps;
        }

        /// <summary>
        /// Interprets the xmldata passed to it as the source XML for an application
        /// XML document.  Returns an Application instance with all of the appropriate
        /// data.
        /// </summary>
        /// <param name="data">The source XML data (not a URL!).</param>
        /// <param name="repo">A reference to the repository instance that this application belongs to.</param>
        /// <param name="url">A URL to be stored in the XmlSource attribute of the Application.  It will not be used to download an XML document.</param>
        /// <returns>The instance of the Application class.</returns>
        internal Application GetApplicationData(byte[] data, Repository repo, string url)
        {
            Tree t = null;
            try
            {
                this.m_XmlReader = XmlReader.Create(new MemoryStream(data));
                t = Tree.FromXml(this.m_XmlReader);
                this.m_XmlReader.Close();
            }
            catch (XmlException)
            {
                // The application data is invalid.
                return null;
            }
            Application app = Application.FetchEmpty();

            app.Repository = repo;
            app.Url = url;
            app.XmlTree = t;

            int autocount = 0;

            // Read the tree to get the application information.
            Node napp = t.GetChildElement("application");

            if (napp == null)
                throw new XmlException("The software.xml document does not contain a root <software> node.");

            foreach (Node setting in napp.Children)
            {
                if (setting.Type != "element")
                    continue;
                   
                switch (setting.Name)
                {
                    case "id":
                        app.ID = setting.GetText().Value;
                        break;
                    case "name":
                        app.Name = setting.GetText().Value;
                        break;
                    case "available":
                        app.Available = (setting.GetText().Value.ToLowerInvariant() == "true");
                        break;
                    case "category":
                        app.Category = setting.GetText().Value;
                        break;
                    case "image":
                        app.Image = new Bitmap(Cache.GetRawData(setting.GetText().Value));
                        app.ImageSource = setting.GetText().Value;
                        break;
                    case "description":
                        app.Description = setting.GetText().Value;
                        break;
                    case "launch":
                        string path = setting.GetText().Value.Trim();
                        if (path.StartsWith("\""))
                        {
                            app.LaunchPath = path.Substring(1, path.Substring(1).IndexOf("\""));
                            app.LaunchArgs = path.Substring(path.Substring(1).IndexOf("\"") + 2).Trim();
                        }
                        else if (path.Contains(" "))
                        {
                            string[] p = path.Trim().Split(new char[] { ' ' }, 2);
                            app.LaunchPath = p[0];
                            app.LaunchArgs = p[1];
                        }
                        else
                        {
                            app.LaunchPath = path.Trim();
                            app.LaunchArgs = "";
                        }
                        break;
                    case "component":
                        // Read all of the subelements to get a list of components
                        // available.
                        Component component = Component.FetchEmpty();

                        // If the application is the official RoketPack application, we implicitly add
                        // an {app} variable (which points outside the default {user} variable).
                        if (app.ID == Manager.RoketPackID && repo.Url == Manager.OfficialRepository)
                        {
                            Variable var = new Variable("app", Directory.GetCurrentDirectory());
                            var.Variables = component.Variables;
                            component.Variables.Add(var);
                        }

                        bool invalid = false;
                        foreach (Node csetting in setting.Children)
                        {
                            switch (csetting.Name)
                            {
                                case "id":
                                    component.ID = csetting.GetText().Value;
                                    break;
                                case "name":
                                    component.Name = csetting.GetText().Value;
                                    break;
                                case "available":
                                    if (invalid) break;
                                    component.Available = (csetting.GetText().Value.ToLowerInvariant() == "true");
                                    break;
                                case "installation":
                                    try
                                    {
                                        if (!this.GetInstallationData(component, csetting))
                                        {
                                            component.Available = false;
                                            invalid = true;
                                        }
                                    }
                                    catch (Exception)
                                    {
                                        component.Available = false;
                                        invalid = true;
                                    }
                                    break;
                            }
                        }
                        if (component.ID != null)
                            app.Components.Add(component.ID, component);
                        else
                        {
                            component.ID = "autocomponent_" + autocount;
                            component.Available = false;
                            app.Components.Add(component.ID, component);
                            autocount += 1;
                        }
                        break;
                }
            }

            return app;
        }

        /// <summary>
        /// Handles interpreting the <installation> section of application data and
        /// putting the read information into the specified component.
        /// </summary>
        /// <param name="component">The component to place the information into.</param>
        /// <param name="node">The <installation> node to read.</param>
        /// <returns>Whether the installation information is valid.</returns>
        private bool GetInstallationData(Component component, Node node)
        {
            List<string> uniquelfiles = new List<string>();
            List<string> uniquelfolders = new List<string>();
            List<string> uniquellinks = new List<string>();

            foreach (Node n in node.Children)
            {
                Node local = null;
                Node remote = null;
                int version = 0;
                int instversion = -1; // We make instversion -1 so that when new files are
                                      // added to a product, the instversion will be less
                                      // then version (which has a minimum value of 0).
                bool deleted = false;
                switch (n.Name)
                {
                    case "variable":
                        string name = n.Attributes["name"];
                        string value = n.GetText().Value;

                        Variable variable = new Variable(name, value);
                        variable.Variables = component.Variables;
                        component.Variables.Add(variable);
                        break;
                    case "file":
                        local = n.GetChildElement("local");
                        remote = n.GetChildElement("remote");
                        bool generated = false;
                        if (n.Attributes.Keys.Contains("generated"))
                            generated = (n.Attributes["generated"].ToLowerInvariant() == "true");

                        // Make sure the entry is valid.
                        if ((remote == null && !generated) || (remote != null && generated) || local == null)
                            return false;
                        if (uniquelfiles.Contains(local.GetText().Value))
                            return false;
                        else
                            uniquelfiles.Add(local.GetText().Value);

                        // Get versioning information
                        if (n.Attributes.Keys.Contains("version"))
                            version = Convert.ToInt32(n.Attributes["version"]);
                        if (n.Attributes.Keys.Contains("instversion"))
                            instversion = Convert.ToInt32(n.Attributes["instversion"]);
                        if (n.Attributes.Keys.Contains("deleted"))
                            deleted = (n.Attributes["deleted"].ToLowerInvariant() == "true");

                        RoketPack.Management.Objects.File f = null;
                        if (generated)
                            f = new RoketPack.Management.Objects.File(local.GetText().Value, null, generated);
                        else
                            f = new RoketPack.Management.Objects.File(local.GetText().Value, remote.GetText().Value, generated);

                        f.Variables = component.Variables;
                        f.AvailableVersion = version;
                        f.InstalledVersion = instversion;
                        f.DeleteOnNextUpdate = deleted;
                        component.Files.Add(f);
                        break;
                    case "folder":
                        local = n.GetChildElement("local");

                        // Make sure the entry is valid.
                        if (local == null)
                            return false;
                        if (uniquelfolders.Contains(local.GetText().Value))
                            return false;
                        else
                            uniquelfolders.Add(local.GetText().Value);

                        // Get versioning information
                        if (n.Attributes.Keys.Contains("version"))
                            version = Convert.ToInt32(n.Attributes["version"]);
                        if (n.Attributes.Keys.Contains("instversion"))
                            instversion = Convert.ToInt32(n.Attributes["instversion"]);
                        if (n.Attributes.Keys.Contains("deleted"))
                            deleted = (n.Attributes["deleted"].ToLowerInvariant() == "true");

                        Folder folder = new Folder(local.GetText().Value);
                        folder.Variables = component.Variables;
                        folder.AvailableVersion = version;
                        folder.InstalledVersion = instversion;
                        folder.DeleteOnNextUpdate = deleted;
                        component.Folders.Add(folder);
                        break;
                    case "link":
                        local = n.GetChildElement("local");
                        Node dest = n.GetChildElement("destination");

                        // Make sure the entry is valid.
                        if (local == null || dest == null)
                            return false;
                        if (uniquellinks.Contains(local.GetText().Value))
                            return false;
                        else
                            uniquellinks.Add(local.GetText().Value);

                        // Get versioning information
                        if (n.Attributes.Keys.Contains("version"))
                            version = Convert.ToInt32(n.Attributes["version"]);
                        if (n.Attributes.Keys.Contains("instversion"))
                            instversion = Convert.ToInt32(n.Attributes["instversion"]);
                        if (n.Attributes.Keys.Contains("deleted"))
                            deleted = (n.Attributes["deleted"].ToLowerInvariant() == "true");

                        Link link = new Link(local.GetText().Value, dest.GetText().Value);
                        link.Variables = component.Variables;
                        link.AvailableVersion = version;
                        link.InstalledVersion = instversion;
                        link.DeleteOnNextUpdate = deleted;
                        component.Links.Add(link);
                        break;
                    case "detection":
                        Detector detector = new Detector(n.GetText().Value, n.Attributes["type"].ToLowerInvariant());
                        detector.Variables = component.Variables;
                        component.Detectors.Add(detector);
                        break;
                    case "classic":
                        Node install = n.GetChildElement("install");
                        Node update = n.GetChildElement("update");
                        Node uninstall = n.GetChildElement("uninstall");

                        // Get versioning information
                        if (n.Attributes.Keys.Contains("version"))
                            version = Convert.ToInt32(n.Attributes["version"]);
                        if (n.Attributes.Keys.Contains("instversion"))
                            instversion = Convert.ToInt32(n.Attributes["instversion"]);

                        component.ClassicInstaller = new Classic(install.GetText().Value, update.GetText().Value, uninstall.GetText().Value);
                        component.ClassicInstaller.Variables = component.Variables;
                        component.ClassicInstaller.AvailableVersion = version;
                        component.ClassicInstaller.InstalledVersion = instversion;
                        break;
                }
            }

            return true;
        }

        /// <summary>
        /// Downloads the repository.xml document for the specified repository (by URL).  Returns
        /// a new Repository object that simply represents the data in the repository (does
        /// not download any application information).
        /// </summary>
        /// <param name="url">The URL of the repository.</param>
        /// <returns>A new Repository object that simply represents the data in the repository (does
        /// not download any application information).</returns>
        internal Repository GetRepository(Uri url)
        {
            Uri u = new Uri(url.ToString() + "/repository.xml");

            // Attempt to retrieve the software.xml file.
            byte[] data = Downloader.Grab(u.ToString());

            if (!Manager.Encoding.GetString(data).StartsWith("<?xml"))
                throw new XmlException();

            // Interpret the repository.xml file that we downloaded.
            string name = null;
            string description = null;
            using (MemoryStream stream = new MemoryStream(data))
            {
                using (XmlReader reader = XmlReader.Create(stream))
                {
                    Tree t = Tree.FromXml(reader);

                    name = t.GetChildElement("repository").GetChildElement("name").GetText().Value;
                    description = t.GetChildElement("repository").GetChildElement("description").GetText().Value;
                }
            }

            return new Repository(url.ToString(), name, description);
        }
    }
}
